import BoundingSphere from '../Core/BoundingSphere.js';
import BoxGeometry from '../Core/BoxGeometry.js';
import Cartesian3 from '../Core/Cartesian3.js';
import combine from '../Core/combine.js';
import defaultValue from '../Core/defaultValue.js';
import defined from '../Core/defined.js';
import destroyObject from '../Core/destroyObject.js';
import DeveloperError from '../Core/DeveloperError.js';
import Matrix4 from '../Core/Matrix4.js';
import VertexFormat from '../Core/VertexFormat.js';
import BufferUsage from '../Renderer/BufferUsage.js';
import DrawCommand from '../Renderer/DrawCommand.js';
import Pass from '../Renderer/Pass.js';
import RenderState from '../Renderer/RenderState.js';
import ShaderProgram from '../Renderer/ShaderProgram.js';
import ShaderSource from '../Renderer/ShaderSource.js';
import VertexArray from '../Renderer/VertexArray.js';
import EllipsoidFS from '../Shaders/EllipsoidFS.js';
import EllipsoidVS from '../Shaders/EllipsoidVS.js';
import BlendingState from './BlendingState.js';
import CullFace from './CullFace.js';
import Material from './Material.js';
import SceneMode from './SceneMode.js';

    var attributeLocations = {
        position : 0
    };

    /**
     * A renderable ellipsoid.  It can also draw spheres when the three {@link EllipsoidPrimitive#radii} components are equal.
     * <p>
     * This is only supported in 3D.  The ellipsoid is not shown in 2D or Columbus view.
     * </p>
     *
     * @alias EllipsoidPrimitive
     * @constructor
     *
     * @param {Object} [options] Object with the following properties:
     * @param {Cartesian3} [options.center=Cartesian3.ZERO] The center of the ellipsoid in the ellipsoid's model coordinates.
     * @param {Cartesian3} [options.radii] The radius of the ellipsoid along the <code>x</code>, <code>y</code>, and <code>z</code> axes in the ellipsoid's model coordinates.
     * @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] The 4x4 transformation matrix that transforms the ellipsoid from model to world coordinates.
     * @param {Boolean} [options.show=true] Determines if this primitive will be shown.
     * @param {Material} [options.material=Material.ColorType] The surface appearance of the primitive.
     * @param {Object} [options.id] A user-defined object to return when the instance is picked with {@link Scene#pick}
     * @param {Boolean} [options.debugShowBoundingVolume=false] For debugging only. Determines if this primitive's commands' bounding spheres are shown.
     *
     * @private
     */
    function EllipsoidPrimitive(options) {
        options = defaultValue(options, defaultValue.EMPTY_OBJECT);

        /**
         * The center of the ellipsoid in the ellipsoid's model coordinates.
         * <p>
         * The default is {@link Cartesian3.ZERO}.
         * </p>
         *
         * @type {Cartesian3}
         * @default {@link Cartesian3.ZERO}
         *
         * @see EllipsoidPrimitive#modelMatrix
         */
        this.center = Cartesian3.clone(defaultValue(options.center, Cartesian3.ZERO));
        this._center = new Cartesian3();

        /**
         * The radius of the ellipsoid along the <code>x</code>, <code>y</code>, and <code>z</code> axes in the ellipsoid's model coordinates.
         * When these are the same, the ellipsoid is a sphere.
         * <p>
         * The default is <code>undefined</code>.  The ellipsoid is not drawn until a radii is provided.
         * </p>
         *
         * @type {Cartesian3}
         * @default undefined
         *
         *
         * @example
         * // A sphere with a radius of 2.0
         * e.radii = new Cesium.Cartesian3(2.0, 2.0, 2.0);
         *
         * @see EllipsoidPrimitive#modelMatrix
         */
        this.radii = Cartesian3.clone(options.radii);
        this._radii = new Cartesian3();

        this._oneOverEllipsoidRadiiSquared = new Cartesian3();
        this._boundingSphere = new BoundingSphere();

        /**
         * The 4x4 transformation matrix that transforms the ellipsoid from model to world coordinates.
         * When this is the identity matrix, the ellipsoid is drawn in world coordinates, i.e., Earth's WGS84 coordinates.
         * Local reference frames can be used by providing a different transformation matrix, like that returned
         * by {@link Transforms.eastNorthUpToFixedFrame}.
         *
         * @type {Matrix4}
         * @default {@link Matrix4.IDENTITY}
         *
         * @example
         * var origin = Cesium.Cartesian3.fromDegrees(-95.0, 40.0, 200000.0);
         * e.modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(origin);
         */
        this.modelMatrix = Matrix4.clone(defaultValue(options.modelMatrix, Matrix4.IDENTITY));
        this._modelMatrix = new Matrix4();
        this._computedModelMatrix = new Matrix4();

        /**
         * Determines if the ellipsoid primitive will be shown.
         *
         * @type {Boolean}
         * @default true
         */
        this.show = defaultValue(options.show, true);

        /**
         * The surface appearance of the ellipsoid.  This can be one of several built-in {@link Material} objects or a custom material, scripted with
         * {@link https://github.com/AnalyticalGraphicsInc/cesium/wiki/Fabric|Fabric}.
         * <p>
         * The default material is <code>Material.ColorType</code>.
         * </p>
         *
         * @type {Material}
         * @default Material.fromType(Material.ColorType)
         *
         *
         * @example
         * // 1. Change the color of the default material to yellow
         * e.material.uniforms.color = new Cesium.Color(1.0, 1.0, 0.0, 1.0);
         *
         * // 2. Change material to horizontal stripes
         * e.material = Cesium.Material.fromType(Cesium.Material.StripeType);
         *
         * @see {@link https://github.com/AnalyticalGraphicsInc/cesium/wiki/Fabric|Fabric}
         */
        this.material = defaultValue(options.material, Material.fromType(Material.ColorType));
        this._material = undefined;
        this._translucent = undefined;

        /**
         * User-defined object returned when the ellipsoid is picked.
         *
         * @type Object
         *
         * @default undefined
         *
         * @see Scene#pick
         */
        this.id = options.id;
        this._id = undefined;

        /**
         * This property is for debugging only; it is not for production use nor is it optimized.
         * <p>
         * Draws the bounding sphere for each draw command in the primitive.
         * </p>
         *
         * @type {Boolean}
         *
         * @default false
         */
        this.debugShowBoundingVolume = defaultValue(options.debugShowBoundingVolume, false);

        /**
         * @private
         */
        this.onlySunLighting = defaultValue(options.onlySunLighting, false);
        this._onlySunLighting = false;

        /**
         * @private
         */
        this._depthTestEnabled = defaultValue(options.depthTestEnabled, true);

        this._useLogDepth = false;

        this._sp = undefined;
        this._rs = undefined;
        this._va = undefined;

        this._pickSP = undefined;
        this._pickId = undefined;

        this._colorCommand = new DrawCommand({
            owner : defaultValue(options._owner, this)
        });
        this._pickCommand = new DrawCommand({
            owner : defaultValue(options._owner, this),
            pickOnly : true
        });

        var that = this;
        this._uniforms = {
            u_radii : function() {
                return that.radii;
            },
            u_oneOverEllipsoidRadiiSquared : function() {
                return that._oneOverEllipsoidRadiiSquared;
            }
        };

        this._pickUniforms = {
            czm_pickColor : function() {
                return that._pickId.color;
            }
        };
    }

    function getVertexArray(context) {
        var vertexArray = context.cache.ellipsoidPrimitive_vertexArray;

        if (defined(vertexArray)) {
            return vertexArray;
        }

        var geometry = BoxGeometry.createGeometry(BoxGeometry.fromDimensions({
            dimensions : new Cartesian3(2.0, 2.0, 2.0),
            vertexFormat : VertexFormat.POSITION_ONLY
        }));

        vertexArray = VertexArray.fromGeometry({
            context : context,
            geometry : geometry,
            attributeLocations : attributeLocations,
            bufferUsage : BufferUsage.STATIC_DRAW,
            interleave : true
        });

        context.cache.ellipsoidPrimitive_vertexArray = vertexArray;
        return vertexArray;
    }

    var logDepthExtension =
        '#ifdef GL_EXT_frag_depth \n' +
        '#extension GL_EXT_frag_depth : enable \n' +
        '#endif \n\n';

    /**
     * Called when {@link Viewer} or {@link CesiumWidget} render the scene to
     * get the draw commands needed to render this primitive.
     * <p>
     * Do not call this function directly.  This is documented just to
     * list the exceptions that may be propagated when the scene is rendered:
     * </p>
     *
     * @exception {DeveloperError} this.material must be defined.
     */
    EllipsoidPrimitive.prototype.update = function(frameState) {
        if (!this.show ||
            (frameState.mode !== SceneMode.SCENE3D) ||
            (!defined(this.center)) ||
            (!defined(this.radii))) {
            return;
        }

        //>>includeStart('debug', pragmas.debug);
        if (!defined(this.material)) {
            throw new DeveloperError('this.material must be defined.');
        }
        //>>includeEnd('debug');

        var context = frameState.context;
        var translucent = this.material.isTranslucent();
        var translucencyChanged = this._translucent !== translucent;

        if (!defined(this._rs) || translucencyChanged) {
            this._translucent = translucent;

            // If this render state is ever updated to use a non-default
            // depth range, the hard-coded values in EllipsoidVS.glsl need
            // to be updated as well.

            this._rs = RenderState.fromCache({
                // Cull front faces - not back faces - so the ellipsoid doesn't
                // disappear if the viewer enters the bounding box.
                cull : {
                    enabled : true,
                    face : CullFace.FRONT
                },
                depthTest : {
                    enabled : this._depthTestEnabled
                },
                // Only write depth when EXT_frag_depth is supported since the depth for
                // the bounding box is wrong; it is not the true depth of the ray casted ellipsoid.
                depthMask : !translucent && context.fragmentDepth,
                blending : translucent ? BlendingState.ALPHA_BLEND : undefined
            });
        }

        if (!defined(this._va)) {
            this._va = getVertexArray(context);
        }

        var boundingSphereDirty = false;

        var radii = this.radii;
        if (!Cartesian3.equals(this._radii, radii)) {
            Cartesian3.clone(radii, this._radii);

            var r = this._oneOverEllipsoidRadiiSquared;
            r.x = 1.0 / (radii.x * radii.x);
            r.y = 1.0 / (radii.y * radii.y);
            r.z = 1.0 / (radii.z * radii.z);

            boundingSphereDirty = true;
        }

        if (!Matrix4.equals(this.modelMatrix, this._modelMatrix) || !Cartesian3.equals(this.center, this._center)) {
            Matrix4.clone(this.modelMatrix, this._modelMatrix);
            Cartesian3.clone(this.center, this._center);

            // Translate model coordinates used for rendering such that the origin is the center of the ellipsoid.
            Matrix4.multiplyByTranslation(this.modelMatrix, this.center, this._computedModelMatrix);
            boundingSphereDirty = true;
        }

        if (boundingSphereDirty) {
            Cartesian3.clone(Cartesian3.ZERO, this._boundingSphere.center);
            this._boundingSphere.radius = Cartesian3.maximumComponent(radii);
            BoundingSphere.transform(this._boundingSphere, this._computedModelMatrix, this._boundingSphere);
        }

        var materialChanged = this._material !== this.material;
        this._material = this.material;
        this._material.update(context);

        var lightingChanged = this.onlySunLighting !== this._onlySunLighting;
        this._onlySunLighting = this.onlySunLighting;

        var useLogDepth = frameState.useLogDepth;
        var useLogDepthChanged = this._useLogDepth !== useLogDepth;
        this._useLogDepth = useLogDepth;

        var colorCommand = this._colorCommand;
        var vs;
        var fs;

        // Recompile shader when material, lighting, or translucency changes
        if (materialChanged || lightingChanged || translucencyChanged || useLogDepthChanged) {
            vs = new ShaderSource({
                sources : [EllipsoidVS]
            });
            fs = new ShaderSource({
                sources : [this.material.shaderSource, EllipsoidFS]
            });
            if (this.onlySunLighting) {
                fs.defines.push('ONLY_SUN_LIGHTING');
            }
            if (!translucent && context.fragmentDepth) {
                fs.defines.push('WRITE_DEPTH');
            }
            if (this._useLogDepth) {
                vs.defines.push('LOG_DEPTH', 'DISABLE_GL_POSITION_LOG_DEPTH');
                fs.defines.push('LOG_DEPTH');
                fs.sources.push(logDepthExtension);
            }

            this._sp = ShaderProgram.replaceCache({
                context : context,
                shaderProgram : this._sp,
                vertexShaderSource : vs,
                fragmentShaderSource : fs,
                attributeLocations : attributeLocations
            });

            colorCommand.vertexArray = this._va;
            colorCommand.renderState = this._rs;
            colorCommand.shaderProgram = this._sp;
            colorCommand.uniformMap = combine(this._uniforms, this.material._uniforms);
            colorCommand.executeInClosestFrustum = translucent;
        }

        var commandList = frameState.commandList;
        var passes = frameState.passes;

        if (passes.render) {
            colorCommand.boundingVolume = this._boundingSphere;
            colorCommand.debugShowBoundingVolume = this.debugShowBoundingVolume;
            colorCommand.modelMatrix = this._computedModelMatrix;
            colorCommand.pass = translucent ? Pass.TRANSLUCENT : Pass.OPAQUE;

            commandList.push(colorCommand);
        }

        if (passes.pick) {
            var pickCommand = this._pickCommand;

            if (!defined(this._pickId) || (this._id !== this.id)) {
                this._id = this.id;
                this._pickId = this._pickId && this._pickId.destroy();
                this._pickId = context.createPickId({
                    primitive : this,
                    id : this.id
                });
            }

            // Recompile shader when material changes
            if (materialChanged || lightingChanged || !defined(this._pickSP) || useLogDepthChanged) {
                vs = new ShaderSource({
                    sources : [EllipsoidVS]
                });
                fs = new ShaderSource({
                    sources : [this.material.shaderSource, EllipsoidFS],
                    pickColorQualifier : 'uniform'
                });
                if (this.onlySunLighting) {
                    fs.defines.push('ONLY_SUN_LIGHTING');
                }
                if (!translucent && context.fragmentDepth) {
                    fs.defines.push('WRITE_DEPTH');
                }
                if (this._useLogDepth) {
                    vs.defines.push('LOG_DEPTH');
                    fs.defines.push('LOG_DEPTH');
                    fs.sources.push(logDepthExtension);
                }

                this._pickSP = ShaderProgram.replaceCache({
                    context : context,
                    shaderProgram : this._pickSP,
                    vertexShaderSource : vs,
                    fragmentShaderSource : fs,
                    attributeLocations : attributeLocations
                });

                pickCommand.vertexArray = this._va;
                pickCommand.renderState = this._rs;
                pickCommand.shaderProgram = this._pickSP;
                pickCommand.uniformMap = combine(combine(this._uniforms, this._pickUniforms), this.material._uniforms);
                pickCommand.executeInClosestFrustum = translucent;
            }

            pickCommand.boundingVolume = this._boundingSphere;
            pickCommand.modelMatrix = this._computedModelMatrix;
            pickCommand.pass = translucent ? Pass.TRANSLUCENT : Pass.OPAQUE;

            commandList.push(pickCommand);
        }
    };

    /**
     * Returns true if this object was destroyed; otherwise, false.
     * <br /><br />
     * If this object was destroyed, it should not be used; calling any function other than
     * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
     *
     * @returns {Boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
     *
     * @see EllipsoidPrimitive#destroy
     */
    EllipsoidPrimitive.prototype.isDestroyed = function() {
        return false;
    };

    /**
     * Destroys the WebGL resources held by this object.  Destroying an object allows for deterministic
     * release of WebGL resources, instead of relying on the garbage collector to destroy this object.
     * <br /><br />
     * Once an object is destroyed, it should not be used; calling any function other than
     * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.  Therefore,
     * assign the return value (<code>undefined</code>) to the object as done in the example.
     *
     * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
     *
     *
     * @example
     * e = e && e.destroy();
     *
     * @see EllipsoidPrimitive#isDestroyed
     */
    EllipsoidPrimitive.prototype.destroy = function() {
        this._sp = this._sp && this._sp.destroy();
        this._pickSP = this._pickSP && this._pickSP.destroy();
        this._pickId = this._pickId && this._pickId.destroy();
        return destroyObject(this);
    };
export default EllipsoidPrimitive;
